package com.domino.server;

import java.util.Date;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.persistence.OptimisticLockException;
import javax.servlet.http.HttpServletRequest;

import com.domino.client.OthelloService;
import com.domino.server.cache.BoardCache;
import com.domino.server.cache.BoardCacheImpl;
import com.domino.server.cache.CachedBoard;
import com.domino.server.cache.GameSession;
import com.domino.server.cache.State;
import com.domino.server.persistence.gamesession.BoardDaoImpl;
import com.domino.server.persistence.gamesession.GameSessionDaoImpl;
import com.domino.server.persistence.playeraudit.PlayerDaoImpl;
import com.domino.server.persistence.playeraudit.PlayerAuditImpl;
import com.domino.server.persistence.playeraudit.PlayerAudit;
import com.domino.server.user.FaceBookGraph;
import com.domino.server.user.FacebookApplication;
import com.domino.server.user.FacebookCookieAuthenticateUtil;
import com.domino.server.user.FacebookGraphImpl;
import com.domino.server.user.FacebookUser;
import com.domino.shared.ActionResponse;
import com.domino.shared.JoinRequest;
import com.domino.shared.JoinResponse;
import com.domino.shared.LeaveGameReason;
import com.domino.shared.Player;
import com.domino.shared.PlayerId;
import com.domino.shared.PutRequest;
import com.domino.shared.SessionStateRequest;
import com.domino.shared.SessionStateResponse;
import com.domino.shared.SessionStateResponseFinished;
import com.domino.shared.SessionStateResponseFirstRoundClientAction;
import com.domino.shared.SessionStateResponseFirstRoundWaitForServer;
import com.domino.shared.SessionStateResponseNewSession;
import com.domino.shared.SessionStateResponsePending;
import com.domino.shared.SessionStateResponsePlayerGaveUp;
import com.domino.shared.SessionStateResponseRoundClientAction;
import com.domino.shared.SessionStateResponseRoundWaitForServer;
import com.domino.shared.model.Board;
import com.domino.shared.model.Brick;
import com.domino.shared.model.Color;
import com.domino.shared.model.Point;
import com.domino.shared.model.SimpleBoard;
import com.domino.shared.session.NoSuchSessionException;
import com.domino.shared.session.SessionId;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * The server side game logic
 */
@SuppressWarnings("serial")
public class OthelloServiceImpl extends RemoteServiceServlet implements
	OthelloService {
	
	private static final long TEN_SECONDS = 10*1000;

	Logger LOG = Logger.getLogger(getClass().getCanonicalName());

	private BoardCache boardCache = new BoardCacheImpl(new GameSessionDaoImpl(), new BoardDaoImpl());
	private FaceBookGraph graph = new FacebookGraphImpl(FacebookApplication.APP_ID, FacebookApplication.SECRET);
	private PlayerAudit playerAudit = new PlayerAuditImpl(new PlayerDaoImpl(), graph);

	@Override
	public void leaveGame(SessionId sessionId, PlayerId playerId, LeaveGameReason reason) throws IllegalArgumentException {
		CachedBoard cachedBoard = boardCache.getBoard(sessionId);
		if (cachedBoard==null) {
			return;
		}
		cachedBoard.leaveGame(playerId);
		try {
			boardCache.updateBoard(cachedBoard);
			if (reason==LeaveGameReason.PlayWithBot) {
				playerAudit.startPlayingWithBot(playerId);
			} else {
				playerAudit.leaveGame(playerId);
			}
		} catch (NoSuchSessionException e) {
			throw new IllegalStateException("Ooops");
		}
	}

	@Override
	public ActionResponse putBrick(PutRequest request) throws IllegalArgumentException, NoSuchSessionException {
		assertRequest(request);
		CachedBoard cachedBoard = boardCache.getBoard(request.getSessionId());
		cachedBoard.putBrick(request.getMove());
		boardCache.updateBoard(cachedBoard);
		if (cachedBoard.isFinished()) {
			this.playerAudit.endPlayingWithOpponent(cachedBoard.getPlayerBlack().getId());
			this.playerAudit.endPlayingWithOpponent(cachedBoard.getPlayerWhite().getId());
			LOG.warning("Game finished " + cachedBoard);
		}
		return new ActionResponse();
	}

	private void assertRequest(Object request) {
		if (request==null) {
			throw new IllegalArgumentException("Request cannot be null");
		}
	}

	@Override
	public JoinResponse joinGame(JoinRequest request)
			throws IllegalArgumentException {
		Player player = getPlayer();
		GameSession gs = joinSession(player, request.getSize());
		return new JoinResponse(gs.getSessionId(), gs.getPlayerBlack(), gs.getPlayerWhite(), gs.getColor(player), gs.getBoard().getSize());
	}

	private GameSession joinSession(Player player, int size) {
		
		CachedBoard session = this.boardCache.findOwnPendingOrRunningGameSession(player.getId());
		
		String message = "JoinSession : running session="+(session==null?"null":session.getSessionId().getId());
		
		if (session==null) {
			session = this.boardCache.findAndJoinPendingGameSession(player);
			if (session==null) {
				session = this.boardCache.createPendingGameSession(player, size);
				verifySession(session);
			} else {
				verifySession(session); // verify before audit
				try {
					this.playerAudit.startPlayingWithOpponent(session.getPlayerBlack().getId());
					this.playerAudit.startPlayingWithOpponent(session.getPlayerWhite().getId());
				} catch(NullPointerException e) {
					LOG.severe("How in hell " +State.decode(session.getState()) + "\n" + session);
				}
			}
			playerAudit.joinGame(player);
			message += ", pending session="+(session==null?"null":session.getSessionId().getId());
		}
		
		LOG.info(message);
		GameSession gs = session.createGameSession();
		return gs;
	}	

	private void verifySession(CachedBoard session) {
		Player black = session.getPlayerBlack();
		Player white = session.getPlayerWhite();
		if (black != null && white != null) {
			if (white.equals(black)) {
				session = boardCache.getBoard(session.getSessionId());
				LOG.severe("Duplicate participant on a table: " + session.getGameSessionData());
				saveAsError(session);
				throw new IllegalStateException("Player got two seats at the session:" + session.getSessionId() + "black=" + black + " white=" + white);
			}
		}
		if (session.isStarted() && (black==null || white==null)) {
			LOG.severe("Started a game with only one player: " + session.getGameSessionData());
			saveAsError(session);
			throw new IllegalStateException("Started a game with only one player: " + session.getSessionId());
		}
	}

	private void saveAsError(CachedBoard session) {
		session.setErrorState();
		try {
			this.boardCache.updateBoard(session);
		} catch (NoSuchSessionException e) {
			throw new IllegalStateException(e);
		} catch (OptimisticLockException e) {
			throw new IllegalStateException("Failed to store error state", e);
		}
	}

	/**
	 * Get Facebook identity or anonymous
	 */
	private Player getPlayer() {
		FacebookCookieAuthenticateUtil futil = new FacebookCookieAuthenticateUtil(graph);
		HttpServletRequest req = this.getThreadLocalRequest();
		FacebookUser fuser = req==null?null:futil.getUser(req);
		
		if (fuser==null) {
			Player p = Player.createAnonymous();
			LOG.warning("Player "+p.getNickname());
			return p;
		} else {
			LOG.warning("Player " + fuser);
			return new Player(new PlayerId(fuser.getId()), fuser.getName(), fuser.getAccessToken());
		}
	}

	@Override
	public SessionStateResponse pollSession(SessionStateRequest request)
			throws IllegalArgumentException, NoSuchSessionException {
		assertRequest(request);
		CachedBoard board = boardCache.getBoard(request.getSessionId());
		if (board==null) {
			throw new IllegalStateException("Could not find board form persistend storage: " + request.getSessionId());
		}
		 
		if (board.isPending()==true) {
			return handlePendingSession(request, board);
		}
		else if (board.isStarted()==true) {
			return handleStartedSession(request, board);
		} else if (board.isFinished()==true) {
			return handleFinishedSession(request, board);
		} else if (board.isPendingTimedOut()==true) { 
			return handlePendingTimedOutSession(request,board);
		} else if (board.isPlayingTimedOut()==true) { 
			return handlePlayingTimedOutSession(request,board);
		} else if (board.hasPlayerGaveUp()==true) {
			return handlePlayerGaveUp(board);
		} else {
			throw new IllegalStateException("Unexpected state " + board);
		}
	}

	private SessionStateResponse handlePlayerGaveUp(CachedBoard board) {
		return new SessionStateResponsePlayerGaveUp(board.getPlayerGivingUp());
	}

	private SessionStateResponse handlePlayingTimedOutSession(
			SessionStateRequest request, CachedBoard board) {
		Color winner =  board.getPlayerBlackTimestamp().after(board.getPlayerWhiteTimestamp())?Color.BLACK:Color.WHITE;
		throw new RuntimeException("Game timed out winner id " + winner.name() + " " + board);
	}

	private SessionStateResponse handlePendingTimedOutSession(SessionStateRequest request, CachedBoard board) {
		// timed out get a new session
		GameSession gs = joinSession(request.getPlayer(), board.getSize());
		Player opponent = request.getPlayer().equals(gs.getPlayerBlack())?gs.getPlayerWhite():gs.getPlayerBlack();
		return new SessionStateResponseNewSession(gs.getSessionId(), opponent, gs.getColor(request.getPlayer()));
	}

	private SessionStateResponse handleFinishedSession(
			SessionStateRequest request, CachedBoard cashedBoard) throws NoSuchSessionException {
		Board board = cashedBoard.getBoard();
		assertBoard2(board, request);
		List<Brick> moves = new LinkedList<Brick>();
		if (request.getNumberOfBricks()<board.getBricks().size()) {
			moves = new LinkedList<Brick>(board.getBricks().subList(request.getNumberOfBricks(), board.getBricks().size()));
		}
		return new SessionStateResponseFinished(moves);
	}

	private SessionStateResponse handleStartedSession(
			SessionStateRequest request, CachedBoard cachedBoard) throws NoSuchSessionException {
		
		Board board = cachedBoard.getBoard();
		assertBoard2(board, request);
		
		String opponent = cachedBoard.getOpponentId(request.getPlayer().getId().getId());
		if (opponent==null) {
			throw new IllegalStateException("No opponent " + cachedBoard);
		}
		Color color = cachedBoard.getColor(request.getPlayer().getId());
		List<Brick> bricks = board.getBricks();
		/* remove this for now, add it again when JPA has been entirely 
		 * replaced by mem-cache
		cachedBoard.updateTimeStamp(color);
		boardCache.updateBoard(cachedBoard);
		*/
		if (bricks.isEmpty()) {
			if (cachedBoard.isStarted()) {
				Player opponentPlayer = cachedBoard.getPlayer(new PlayerId(opponent));
				if (cachedBoard.getColor(request.getPlayer().getId()).equals(Color.BLACK) ) {
					return new SessionStateResponseFirstRoundClientAction(cachedBoard.getSessionId(), opponentPlayer);
				} else {
					return new SessionStateResponseFirstRoundWaitForServer(cachedBoard.getSessionId(), opponentPlayer);
				}
				
			} else {
				throw new IllegalStateException("How did we get here??? " + cachedBoard);
				//return new SessionStateResponse(SessionStateResponse.WAIT_FOR_OPPONENT_ACTION,move,opponent);
			}
		}
		
		List<Brick> moves = new LinkedList<Brick>();
		if (request.getNumberOfBricks()<board.getBricks().size()) {
			moves = new LinkedList<Brick>(board.getBricks().subList(request.getNumberOfBricks(), board.getBricks().size()));
		}
		if (request.getNumberOfBricks()<board.getBricks().size()) {
			if (request.getNumberOfBricks()+1<board.getBricks().size()) {
				moves = new LinkedList<Brick>(board.getBricks().subList(request.getNumberOfBricks(), board.getBricks().size()));
				return new SessionStateResponseRoundWaitForServer(moves); // catch up!
			}
		}
		
		if (board.getAvailblePoints(Color.getOpposite(color)).isEmpty() && 
				board.getAvailblePoints(color).isEmpty()) {
			return new SessionStateResponseFinished(moves);
		}
		if (bricks.get(bricks.size()-1).getColor().equals(color)==false) {
			if (board.getAvailblePoints(color).isEmpty()) {
				return new SessionStateResponseRoundWaitForServer(moves);
			} else {
				return new SessionStateResponseRoundClientAction(moves);
			}
		} else {
			if (board.getAvailblePoints(Color.getOpposite(color)).isEmpty()) {
				return new SessionStateResponseRoundClientAction(moves);
			} else {
				return new SessionStateResponseRoundWaitForServer(moves);
			}
		}
	}

	private SessionStateResponse handlePendingSession(SessionStateRequest request, CachedBoard board) throws NoSuchSessionException {
		String opponent = board.getOpponentId(request.getPlayer().getId().getId());
		if (opponent!=null) {
			throw new IllegalStateException("Cannot be an opponent in pending state " + board);
		}
		Color color = board.getColor(request.getPlayer().getId());
		if (color==null) {
			String message = request.toString() + "\n" + board.toString();
			throw new IllegalStateException("Player not joind to game that was polled: request=" + message);
		} 
		Date tenSecondsAgo = new Date(System.currentTimeMillis() - TEN_SECONDS);
		if (board.getLastUpdated().before(tenSecondsAgo)
			&& boardCache.getPendingSessionCount()>1) {
			board.setPendingTimeout();
			boardCache.updateBoard(board);
		} else {
			board.updateTimeStamp(color);
			boardCache.updateBoard(board);
		}
		return new SessionStateResponsePending();
	}

	private void assertBoard2(Board board, SessionStateRequest request) throws NoSuchSessionException {
					if (board==null) {
				LOG.warning("Failed to find board for request:" + request);
				throw new NoSuchSessionException("Failed to find board for request with session id:" + request.getSessionId());
			}

	}

	@Override
	public List<Brick> getBricks(SessionId id) throws IllegalArgumentException, NoSuchSessionException {
		CachedBoard cachedBoard = boardCache.getBoard(id);
		if (cachedBoard == null) {
			throw new NoSuchSessionException("Could no find board with id " + id.getId());
		}
		
		return cachedBoard.getBoard().getBricks();
	}

	@Override
	public void dummy(SessionStateResponseRoundWaitForServer a1,
			SessionStateResponseRoundClientAction a2,
			SessionStateResponsePending a3, SessionStateResponseNewSession a4,
			SessionStateResponseFirstRoundWaitForServer a5,
			SessionStateResponseFirstRoundClientAction a6,
			SessionStateResponseFinished a7,
			SessionStateResponsePlayerGaveUp a8, PlayerId id, SimpleBoard sb,
			LinkedList<Point> list, LinkedHashSet<Point> set,
			LinkedHashSet<Point> list2, LinkedList<Point> list3)
			throws IllegalArgumentException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void botGameFinished(SessionId id, PlayerId playerId, String bricks)
			throws IllegalArgumentException {
		LOG.warning("Botgame finished: "+ id + " : " + playerId + " : " + bricks);
		this.playerAudit.endPlayingWithBot(playerId);
	}

	
}

